import type {
    DomEventHandler,
    LineStringGeometry,
    LngLat,
    PolygonGeometry,
    MMapMarkerEventHandler,
    MMapMarkerProps
} from '@mappable-world/mappable-types';
import type {Reactify} from '@mappable-world/mappable-types/reactify';
import {loopLineString} from './common';
import {DEFAULT_FEATURE_LINE_STYLE, DEFAULT_FEATURE_POLYGON_STYLE} from './variables';

const {useContext, useCallback, useEffect, useState, useMemo, useRef, forwardRef} = React;

const Z_INDEX_BASE = 2600;
const Z_INDEX_POLYGON = Z_INDEX_BASE;
const Z_INDEX_LINE = Z_INDEX_BASE + 0.1;
const Z_INDEX_PREVIEW_MARKERS = -1;
const Z_INDEX_MARKERS = Z_INDEX_BASE + 0.3;

const LINE_FEATURE_ID = 'GEOMETRY_EDITOR_LINE_FEATURE';
const PREVIEW_MARKER_FEATURE_ID = 'GEOMETRY_EDITOR_PREVIEW_MARKER_FEATURE';

export const MMapsReactifyContext = React.createContext<Reactify | null>(null);

export type GeometryEditorGeometry = LineStringGeometry | PolygonGeometry;

export type GeometryEditorProps = {
    geometry: GeometryEditorGeometry;
    onChange?: (geometry: GeometryEditorGeometry) => void | false;
    maxVertex?: number;
    minVertex?: number;
    editMode?: boolean;
};

type GeometryEditorPoint = {
    coordinates: LngLat;
    key: string;
};

export const GeometryEditor = ({geometry, editMode = false, onChange, minVertex, maxVertex}: GeometryEditorProps) => {
    const reactify = useContext(MMapsReactifyContext);
    const MMapFeature = reactify.entity(mappable.MMapFeature);
    const MMapListener = reactify.entity(mappable.MMapListener);

    const id = useRef(0);
    const keyOrder = useRef<string[]>([]);
    const keyCoordinates = useRef<Record<string, LngLat>>({});
    const previewMarkerRef = useRef();

    const createNewKey = () => (id.current++).toString();

    const [previewCoordinates, setPreviewCoordinates] = useState<LngLat>();
    const [previewSegmentIndex, setPreviewSegmentIndex] = useState<number>();
    const [previewPointDrag, setPreviewPointDrag] = useState(false);

    const closed = useMemo(() => geometry.type === 'Polygon', [geometry]);

    const points = useMemo<GeometryEditorPoint[]>(() => {
        const coordinates: LngLat[] =
            geometry.type === 'LineString' ? geometry.coordinates : geometry.coordinates[0].slice(0, -1);

        keyOrder.current.length = coordinates.length;

        return coordinates.map((coordinates, index) => {
            let key: string;
            if (keyOrder.current[index] !== undefined) {
                key = keyOrder.current[index];
            } else {
                key = createNewKey();
                keyOrder.current[index] = key;
            }
            keyCoordinates.current[key] = coordinates;
            return {key, coordinates};
        });
    }, [geometry, maxVertex, minVertex]);

    const isMaxVertex = useMemo(() => maxVertex !== undefined && points.length >= maxVertex, [points, maxVertex]);
    const isMinVertex = useMemo(() => minVertex !== undefined && points.length <= minVertex, [points, minVertex]);

    const lineGeometry = useMemo<LineStringGeometry>(() => {
        const coordinates = points.map((p) => p.coordinates);
        return {type: 'LineString', coordinates: closed ? loopLineString(coordinates) : coordinates};
    }, [points, closed]);

    const polygonGeometry = useMemo<PolygonGeometry>(
        () => ({type: 'Polygon', coordinates: [points.map((p) => p.coordinates)]}),
        [points]
    );

    const updatePoint = () => {
        const coordinates = keyOrder.current.map((key) => keyCoordinates.current[key]);
        const geometry: GeometryEditorGeometry = closed
            ? (turf.polygon([loopLineString(coordinates)]).geometry as PolygonGeometry)
            : (turf.lineString(coordinates).geometry as LineStringGeometry);
        onChange?.(geometry);
    };

    const onMapClick = useCallback<DomEventHandler>(
        (object, event) => {
            if (object !== undefined && object.entity === previewMarkerRef.current) return;
            if (isMaxVertex) return;

            const newKey = createNewKey();
            keyOrder.current.push(newKey);
            keyCoordinates.current[newKey] = event.coordinates;
            updatePoint();
        },
        [isMaxVertex]
    );

    const onMapMouseMove = useCallback<DomEventHandler>(
        (object, event) => {
            if (object === undefined) return;

            if (object.type === 'feature' && object.entity.id === LINE_FEATURE_ID) {
                setPreviewCoordinates(event.coordinates);
            }
            if (object.type === 'marker' && object.entity === previewMarkerRef.current && !previewPointDrag) {
                const pointOnLineCoordinates = turf.nearestPointOnLine(lineGeometry, event.coordinates);
                setPreviewCoordinates(pointOnLineCoordinates.geometry.coordinates as LngLat);
                setPreviewSegmentIndex(pointOnLineCoordinates.properties.index);
            }
        },
        [lineGeometry, previewPointDrag]
    );

    const onClickPreviewPoint = useCallback<MMapMarkerProps['onClick']>(
        (_, {coordinates}) => {
            const newKey = createNewKey();
            keyOrder.current.splice(previewSegmentIndex + 1, 0, newKey);
            keyCoordinates.current[newKey] = coordinates;
            updatePoint();
        },
        [isMaxVertex, previewSegmentIndex]
    );

    const onDragMovePreviewPoint = useCallback<MMapMarkerEventHandler>(
        (coordinates) => {
            const key = keyOrder.current[previewSegmentIndex + 1];
            keyCoordinates.current[key] = coordinates;
            setPreviewCoordinates(coordinates);
            updatePoint();
        },
        [previewSegmentIndex]
    );

    const onDragStartPreviewPoint = useCallback<MMapMarkerEventHandler>(
        (coordinates) => {
            const newKey = createNewKey();
            keyOrder.current.splice(previewSegmentIndex + 1, 0, newKey);
            keyCoordinates.current[newKey] = coordinates;

            setPreviewPointDrag(true);
            updatePoint();
        },
        [previewSegmentIndex]
    );

    const onDragEndPreviewPoint = useCallback<MMapMarkerEventHandler>((coordinates) => {
        setPreviewPointDrag(false);
    }, []);

    const onDragMovePolygonFeature = useCallback((geometryCoordinates: LngLat[][]) => {
        const {geometry} = turf.polygon([loopLineString(geometryCoordinates[0])]);
        onChange?.(geometry as GeometryEditorGeometry);
    }, []);

    const renderPoints = useMemo(
        () =>
            points.map((point, index) => {
                const onDragMove = (coordinates: LngLat) => {
                    keyCoordinates.current[point.key] = coordinates;
                    updatePoint();
                };

                const onDoubleClick = () => {
                    if (!editMode || isMinVertex) {
                        return;
                    }

                    keyCoordinates.current[point.key] = undefined;
                    keyOrder.current.splice(index, 1);
                    updatePoint();
                };
                return {onDragMove, onDoubleClick, ...point};
            }),
        [points, isMinVertex, editMode]
    );

    return (
        <>
            {editMode && <MMapListener onClick={onMapClick} onMouseMove={onMapMouseMove} />}
            <MMapFeature
                id={LINE_FEATURE_ID}
                zIndex={Z_INDEX_LINE}
                geometry={lineGeometry}
                style={DEFAULT_FEATURE_LINE_STYLE}
            />
            {closed && (
                <MMapFeature
                    zIndex={Z_INDEX_POLYGON}
                    geometry={polygonGeometry}
                    style={DEFAULT_FEATURE_POLYGON_STYLE}
                    draggable={editMode}
                    onDragMove={onDragMovePolygonFeature}
                />
            )}
            {editMode && renderPoints.map((point) => <GeometryEditorMarker draggable={editMode} {...point} />)}

            {editMode && previewCoordinates && (!isMaxVertex || previewPointDrag) && (
                <GeometryEditorPreviewMarker
                    coordinates={previewCoordinates}
                    draggable={editMode}
                    onClick={onClickPreviewPoint}
                    onDragMove={onDragMovePreviewPoint}
                    onDragStart={onDragStartPreviewPoint}
                    onDragEnd={onDragEndPreviewPoint}
                    ref={previewMarkerRef}
                />
            )}
        </>
    );
};

const GeometryEditorMarker = forwardRef((props: MMapMarkerProps, ref) => {
    const reactify = useContext(MMapsReactifyContext);
    const MMapMarker = reactify.entity(mappable.MMapMarker);

    return (
        <MMapMarker {...props} zIndex={Z_INDEX_MARKERS} ref={ref}>
            <div className="geometry-editor-marker"></div>
        </MMapMarker>
    );
});

const GeometryEditorPreviewMarker = forwardRef((props: MMapMarkerProps, ref) => {
    const reactify = useContext(MMapsReactifyContext);
    const MMapMarker = reactify.entity(mappable.MMapMarker);

    return (
        <MMapMarker {...props} zIndex={Z_INDEX_PREVIEW_MARKERS} id={PREVIEW_MARKER_FEATURE_ID} ref={ref}>
            <div className="geometry-editor-marker preview"></div>
        </MMapMarker>
    );
});
